4.6 Der Destruktor
 
Ein Konstruktor wird aufgerufen, wenn das Objekt einer Klasse erzeugt wird. Damit beginnt der Lebenszyklus des Objekts, der dann endet, wenn das Pendant des Konstruktors aufgerufen wird: der Destruktor.
4.6.1 Das Zerstören von Objekten
 
Im Destruktor sind oft Anweisungen enthalten, um die von einem Objekt zusätzlich beanspruchten Fremdressourcen freizugeben, die beispielsweise andere Systeme belasten. Dazu gehören unter anderem Netzwerk- oder Datenbankverbindungen, die in einer objektinternen Referenz vorgehalten werden.
Der Umstand, der zur Aufgabe eines konkreten Objekts und dem damit verbundenen Destruktoraufruf führt, kann
|
das Verlassen des Gültigkeitsbereichs der Objektvariablen oder |
|
die Zuweisung von null an die Objektreferenz |
sein. Das bedeutet jedoch nicht, dass beim Eintreten einer dieser beiden Bedingungen augenblicklich der Destruktor ausgeführt wird. Tatsächlich kann das noch eine unbestimmbare Zeit dauern. Mit anderen Worten bedeutet dies, dass das Objekt zwar aus Sicht des Programms nicht mehr existiert, sich jedoch immer noch im Speicher befindet und diesen letztendlich belastet.
4.6.2 Der Garbage Collector
 
Den Anstoß zum Aufruf des Destruktors gibt ein Mechanismus, der als Garbage Collector (GC) bezeichnet wird. Der Garbage Collector hat die Aufgabe, nach den Objekten zu suchen, die nicht mehr referenziert werden. Diese werden dann endgültig verworfen, d. h., der von den Objekten reservierte Speicherplatz wird freigegeben und kann anschließend von anderen Objekten beansprucht werden. Während des Prozesses komprimiert der Garbage Collector den Heap, so dass eine Fragmentierung die Vergabe von Speicher nicht verhindert, weil kein ausreichend großer Speicherblock zur Verfügung steht.
Wie bereits erwähnt, kann nicht vorhergesagt werden, wann der GC seine Arbeit aufnimmt. Damit stellt sich sofort die Frage, nach welchen Kriterien der GC aktiv wird. Als selbstständige Ausführungseinheit (Thread) genießt der GC keine hohe Priorität und kann erst dann den Prozessor in Anspruch nehmen, wenn die Anwendung beschäftigungslos ist. Theoretisch könnte das bedeuten, dass eine viel beschäftigte Anwendung dem GC keine Chance lässt, jemals aktiv zu werden. Dem ist tatsächlich so, es gibt aber eine wichtige Einschränkung: Noch bevor den Speicherressourcen der Anwendung die »Luft ausgeht«, ist die zweite Bedingung erfüllt, um die Speicherbereinigung mit dem GC anzustoßen.
| Der Garbage Collector wird spätestens dann nach allen aufgegebenen Objekten suchen und deren Speicherplatz freigeben, wenn die Speicherressourcen knapp werden.
|
Der Anstoß dazu kann auch im Programmcode unter Aufruf der statischen Methode Collect der Klasse GC gegeben werden:
Die Suche nach allen freigegebenen Objekten nimmt natürlich Zeit in Anspruch und führt zu Performanceverlusten der laufenden Anwendung. Daher ist es nicht empfehlenswert, ohne ein gewichtiges Argument bei jeder Objektzerstörung »zur Sicherheit« zusätzlich den Bereinigungsprozess zu aktivieren.
4.6.3 Die Bereitstellung eines Destruktors
 
Die Syntax eines Destruktors lautet wie folgt:
| ~Klassenbezeichner() {/*...*/}
|
Eingeleitet wird der Destruktor mit dem Tildezeichen, danach folgen der Klassenbezeichner mit dem obligatorischen runden Klammerpaar und letztendlich der Anweisungsblock. Ein Destruktor enthält weder einen Zugriffsmodifizierer noch eine Parameterliste oder die Angabe eines Rückgabetyps.
4.6.4 Das Zerstören eines Objekts
 
Ob ein Destruktor implementiert ist, wird schon während der Instanziierung der Klasse festgestellt, und das Objekt wird markiert. Die Common Language Runtime unterstützt eine Warteschlange, in der die Referenzen auf alle entsprechenden Objekte enthalten sind. Vor der Zerstörung eines freigegebenen Objekts prüft der GC, ob das Objekt einen Destruktor enthält. Falls ja, wird dieser ausgeführt und anschließend der vom Objekt reservierte Speicher freigegeben. Dieser Prozess hat insbesondere dann eine schlechtere Performance der Anwendung zur Folge, wenn sehr viele Objekte einen Destruktor haben. Daher gilt die folgende Regel:
| Implementieren Sie den Destruktor nur dann, wenn er auch tatsächlich benötigt wird.
|
4.6.5 Die »Dispose«-Methode
 
Mit den Destruktoren sind zwei gravierende Nachteile verbunden:
|
Wenn er implementiert ist, kann nicht exakt vorherbestimmt werden, wann er vom Speicherbereinigungsprozess ausgeführt wird. |
|
Er kann nicht explizit aufgerufen werden, auch nicht von der Klasse, in der er implementiert ist. |
Wie Sie bereits wissen, werden die Aufräumarbeiten angestoßen, wenn durch die Beschäftigungslosigkeit einer laufenden Anwendung der niedrig prioritäre Thread des Garbage Collectors seine Arbeit aufnimmt oder sich die Speicherressourcen verknappen. Tatsächlich sind sogar Situationen denkbar, die niemals zum Destruktoraufruf führen – denken Sie nur an den Absturz des Rechners. Folglich kann auch nicht garantiert werden, dass der GC überhaupt jemals seine ihm angedachte Aufgabe verrichtet. Wenn ein Objekt aber kostspielige oder begrenzte Ressourcen beansprucht, muss sichergestellt sein, dass diese so schnell wie möglich wieder freigegeben werden.
Um dem Problem zu begegnen, können Sie, auch zusätzlich zum Destruktor, eine öffentliche Methode implementieren, die der Benutzer der Klasse aufrufen kann. Diese Methode heißt vereinbarungsgemäß Dispose. Damit aber nicht genug. Wir müssen thematisch noch einen Riesenschritt nach vorne machen und unsere Klasse um die Schnittstelle IDisposable erweitern. Eine Schnittstellendefinition ähnelt einer Klassendefinition, jedoch gibt es besondere Richtlinien, die sowohl die Implementierung als auch den Nutzen einer Schnittstelle beschreiben. Wir werden uns diesen in Kapitel 6 zuwenden. Auf das Verständnis der Wirkungsweise der Dispose-Methode haben sie keinen Einfluss.
Schauen wir uns zunächst die Struktur einer Klasse mit Dispose-Methode an:
| class ClassA : IDisposable {
|
| ...
|
| public void Dispose() {
|
| // Anweisungen
|
| }
|
| }
|
Eine Klasse implementiert eine Schnittstelle, indem hinter dem Klassenbezeichner, getrennt durch einen Doppelpunkt, der Schnittstellenbezeichner angegeben wird. Jede Methode, die von einer Schnittstelle veröffentlicht wird, muss in der Klasse, die sich der Dienste der Schnittstelle bedient, implementiert werden. IDisposable enthält nur die Methode Dispose. Die obige Codestruktur erfüllt damit alle Forderungen.
Die Dispose-Methode soll dem Client die Sicherheit geben, nicht auf den GC und damit auf die Ausführung des Destruktors warten zu müssen. Also muss der Code, der die beanspruchten Eigen- oder Fremdressourcen freigibt, in der Methode Dispose implementiert werden.
Zwischen dem Destruktor und der Dispose-Methode besteht ein gravierender Unterschied, der die weitere Vorgehensweise bei der Programmierung nachhaltig beeinflusst: Während der Destruktor nur vom GC aufgerufen werden kann, erfordert die Ausführung von Dispose den expliziten Aufruf:
| ClassA obj = new ClassA();
|
| ...
|
| obj.Dispose();
|
Die Garantie, dass der Benutzer den Aufruf startet, kann aber niemand geben. Daran muss bei der Entwicklung einer Klasse gedacht werden. Es gibt nur eine logische Konsequenz: Der erforderliche Code muss sowohl in Dispose als auch im Destruktor implementiert werden.
Wir müssen nun dem Umstand Rechnung tragen, dass der Destruktor nicht aus Dispose heraus ausgerufen werden kann, der Aufruf von Dispose aus dem Destruktor heraus jedoch möglich ist. Das soll nun auch implementiert werden:
| class ClassA : IDisposable {
|
| public void Dispose() {
|
| // Freigabe der Fremdressourcen
|
| }
|
| // Destruktor
|
| ~ClassA() {
|
| Dispose();
|
| }
|
| }
|
Ein Problem dieses Codefragments ist, dass die Methode Dispose nicht nur durch den Destruktor aufgerufen wird. Sie könnte zuvor auch aus dem Code heraus aufgerufen worden sein. Das hätte den Versuch zur Folge, die Ressourcen ein zweites Mal freizugeben, und kann zu einem undefinierten Programmzustand führen.
Um dieses Problem zu meistern, bietet sich eine Methode der Klasse GC an: SuppressFinalize. Dieser wird beim Aufruf als Argument die Referenz auf das Objekt übergeben, dessen Destruktor nicht mehr ausgeführt werden soll. In unserem Fall handelt es sich um die aktuelle Instanz, die durch das Schlüsselwort this beschrieben wird.
| public void Dispose() {
|
| // Freigabe der Fremdressourcen
|
| GC.SuppressFinalize(this);
|
| }
|
Zum Abschluss unserer Betrachtungen gilt es, noch zwei Situationen zu berücksichtigen, die im Zusammenhang mit der Implementierung von Dispose während der Laufzeit auftreten könnten und entsprechend behandelt werden müssen:
|
Die Dispose-Methode eines Objekts könnte vom Benutzer wiederholt aufgerufen werden. Das führt zur wiederholten Freigabe der Ressourcen mit der oben erwähnten Gefahr eines undefinierten Programmzustands. |
|
Nach der Ausführung von Dispose könnte eine andere Methode des Objekts aufgerufen werden, die ihrerseits versucht, auf die inzwischen freigegebene Ressource zuzugreifen. |
Im folgenden Codefragment wird gezeigt, wie Sie diesen Problemen erfolgreich begegnen können. Dazu wird eine fiktive Klasse Connection instanziiert, von der wir annehmen, dass sie Ressourcen auf einem entfernten Rechner reserviert.
| class ClassA : IDisposable {
|
| private bool disposed = false;
|
| private Connection con = new Connection();
|
| public void DoSomething() {
|
| if(this.disposed)
|
| // falls das Objekt bereits freigegeben ist, darauf reagieren
|
| // hier: Auslösen einer Ausnahme
|
| throw new DisposedException();
|
| // weiterer Programmcode
|
| }
|
| public void Dispose() {
|
| if(! this.disposed) {
|
| // Freigabe der Fremdressourcen
|
| con = null;
|
| disposed = true;
|
| GC.SuppressFinalize(this);
|
| }
|
| }
|
| ~ClassA() {
|
| Dispose();
|
| }
|
| }
|
Um sicherzustellen, dass die Ressource nicht wiederholt freigegeben wird, ist die private boolesche Variable disposed deklariert. Betrachten wir den Ablauf zur Laufzeit und da zunächst den Fall, dass der Benutzer die Methode Dispose eines ClassA-Objekts zum ersten Mal aufruft. Zunächst wird der Inhalt des Flags disposed geprüft. Dieser ist false, daher wird die Ressource freigegeben und das Flag anschließend auf true gesetzt. Abgeschlossen wird Dispose mit dem Methodenaufruf SuppressFinalize der Klasse GC.
Nun könnte der Benutzer fälschlicherweise erneut auf die Idee kommen, die Dispose-Methode des Objekts anzusteuern. Dabei gehen wir davon aus, dass der GC zwischenzeitlich das Objekt noch nicht der endgültigen Zerstörung zugeführt hat. Die Prüfung des Flags disposed, dessen Inhalt nun true ist, wird durch die bedingte Anweisung zu einer Abweisung führen, und der Methodenaufruf wird sofort beendet.
Betrachten wir nun noch den letzten Fall. Die im obigen Codefragment der Klasse implementierte Methode DoSomething symbolisiert eine Funktionalität, welche die Fremdressource in Anspruch nimmt. Wird DoSomething aufgerufen, nachdem es keine gültige Referenz auf das Objekt con mehr gibt, ist die Folge ein Laufzeitfehler – die Anwendung wird außerplanmäßig beendet. Um das zu vermeiden, wird in der Methode eine Ausnahme ausgelöst, die im aufrufenden Code behandelt werden muss. Mit der Implementierung von Ausnahmebehandlungen werden wir uns in Kapitel 9 eingehend beschäftigen.
Die »using«-Anweisung zur Zerstörung von Objekten
C# stellt eine alternative Möglichkeit bereit, ein Objekt schnellstmöglich zu zerstören. Es handelt sich hierbei um das Schlüsselwort using, das in diesem Fall nicht als Direktive, sondern als Anweisung eingesetzt wird. Die Syntax dazu lautet:
| using(Ausdruck) {
|
| // Anweisungen
|
| }
|
Im Ausdruck wird ein Objekt instanziiert, auf das nach dem Verlassen des Anweisungsblocks automatisch die Dispose-Methode aufgerufen wird.
| using (ClassA obj = new ClassA()) {
|
| obj.AnyOperation();
|
| }
|
Die Klasse, die im Ausdruck instanziiert wird, muss eine Bedingung erfüllen: Sie muss die Schnittstelle IDisposable implementieren.
4.6.6 Der Garbage Collector in Aktion
 
Zum Abschluss wollen wir natürlich auch einmal den Garbage Collector bei seiner Arbeit beobachten. Dazu wird im folgenden Beispiel die Klasse GCTest definiert, die den Destruktor implementiert, bei dessen Aufruf eine Meldung an der Konsole ausgegeben wird, die eine interne Objektkennnummer enthält.
| // --------------------------------------------------------------
|
| // Beispiel: ...\Kapitel 4\GarbageCollector
|
| // --------------------------------------------------------------
|
| class Program {
|
| GCTest testObj1;
|
| static void Main(string[] args) {
|
| Program myCls = new Program();
|
| myCls.TestProc();
|
| }
|
| public void TestProc() {
|
| testObj1 = new GCTest(1);
|
| GCTest testObj3 = new GCTest(3);
|
| testObj3 = null;
|
| this.CreateNewObject();
|
| Console.ReadLine();
|
| GC.Collect();
|
| Console.ReadLine();
|
| }
|
| public void CreateNewObject() {
|
| GCTest testObj2 = new GCTest(2);
|
| }
|
| }
|
| class GCTest {
|
| private int number;
|
| // Konstruktor
|
| public GCTest(int x) {
|
| number = x;
|
| }
|
| // Destruktor
|
| ~GCTest() {
|
| Console.WriteLine("Objekt-Nr.{0} zerstört.", number);
|
| }
|
| }
|
Im Code sind drei verschiedene Objektvariablen deklariert: ein Feld auf Klassenebene (testObj1) sowie zwei lokale Referenzen in TestProc (testObj2 und testObj3). Für jede Referenz ist eine andere Lebensdauer festgelegt: testObj1 wird zerstört, wenn die Anwendung beendet wird, testObj2 nach dem Verlassen der Prozedur NewObjec, und die Zerstörung von testObj3 wird mit null explizit angestoßen. Um den Garbage Collector zur Aktivität anzuspornen, wird in Main die Methode Collect der Klasse GC aufgerufen.
Wenn Sie die Anwendung starten, müssen Sie durch Drücken der (Enter)-Taste während der Laufzeit die Collect-Methode anstoßen, um die Meldung der beiden Destruktoren der Objekte testOb2 und testObj3zu sehen. Die Meldung des letzten Objekts können Sie nach einer weiteren Betätigung der (Enter)-Taste noch soeben erkennen, bevor das Konsolenfenster geschlossen wird.
4.6.7 Zusammenfassung
 
|
Ein Destruktor ist das Gegenstück zu den Konstruktoren. Beschrieben wird ein Destruktor durch ein Tildezeichen (~), dem der Klassenname folgt. Destruktoren haben weder einen Zugriffsmodifizierer noch eine Parameterliste. |
|
Enthält die Typdefinition eines Objekts einen Destruktor, wird dessen Code ausgeführt, wenn das Objekt freigegeben worden ist und von der Garbage Collection erfasst wird. |
|
Der Garbage Collector läuft in einem systemeigenen Thread niedriger Priorität. Er wird unter zwei Bedingungen ausgeführt: entweder wenn die Speicherressourcen knapp werden oder wenn die Anwendung beschäftigungslos ist. |
|
Der Destruktor kann nicht explizit aufgerufen werden. Damit Code dennoch die Möglichkeit hat, den Destruktors zu nutzen, sollte die Schnittstelle IDisposable mit ihrer Methode Dispose implementiert werden. |
|
Mit der Klasse GC hat ein Entwickler die Möglichkeit, den Garbage Collector zu steuern. Insbesondere sind hier die Methoden Collect und SuppressFinalize zu erwähnen. Mit Collect wird der GC angestoßen, die ihm zugedachte Aufgabe zu erledigen, und mit SuppressFinalize wird dem GC mitgeteilt, den Destruktor eines bestimmten Objekts nicht mehr zu berücksichtigen. |
|